{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "13475b38",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Requirement already satisfied: spacy in d:\\program files\\anaconda3\\lib\\site-packages (3.8.4)\n",
      "Requirement already satisfied: spacy-legacy<3.1.0,>=3.0.11 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (3.0.12)\n",
      "Requirement already satisfied: spacy-loggers<2.0.0,>=1.0.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (1.0.5)\n",
      "Requirement already satisfied: murmurhash<1.1.0,>=0.28.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (1.0.12)\n",
      "Requirement already satisfied: cymem<2.1.0,>=2.0.2 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (2.0.11)\n",
      "Requirement already satisfied: preshed<3.1.0,>=3.0.2 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (3.0.9)\n",
      "Requirement already satisfied: thinc<8.4.0,>=8.3.4 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (8.3.4)\n",
      "Requirement already satisfied: wasabi<1.2.0,>=0.9.1 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (1.1.3)\n",
      "Requirement already satisfied: srsly<3.0.0,>=2.4.3 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (2.5.1)\n",
      "Requirement already satisfied: catalogue<2.1.0,>=2.0.6 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (2.0.10)\n",
      "Requirement already satisfied: weasel<0.5.0,>=0.1.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (0.4.1)\n",
      "Requirement already satisfied: typer<1.0.0,>=0.3.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (0.15.2)\n",
      "Requirement already satisfied: tqdm<5.0.0,>=4.38.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (4.65.0)\n",
      "Requirement already satisfied: numpy>=1.19.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (1.23.5)\n",
      "Requirement already satisfied: requests<3.0.0,>=2.13.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (2.31.0)\n",
      "Requirement already satisfied: pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (2.10.6)\n",
      "Requirement already satisfied: jinja2 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (3.1.2)\n",
      "Requirement already satisfied: setuptools in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (68.0.0)\n",
      "Requirement already satisfied: packaging>=20.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (23.0)\n",
      "Requirement already satisfied: langcodes<4.0.0,>=3.2.0 in d:\\program files\\anaconda3\\lib\\site-packages (from spacy) (3.5.0)\n",
      "Requirement already satisfied: language-data>=1.2 in d:\\program files\\anaconda3\\lib\\site-packages (from langcodes<4.0.0,>=3.2.0->spacy) (1.3.0)\n",
      "Requirement already satisfied: annotated-types>=0.6.0 in d:\\program files\\anaconda3\\lib\\site-packages (from pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4->spacy) (0.7.0)\n",
      "Requirement already satisfied: pydantic-core==2.27.2 in d:\\program files\\anaconda3\\lib\\site-packages (from pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4->spacy) (2.27.2)\n",
      "Requirement already satisfied: typing-extensions>=4.12.2 in d:\\program files\\anaconda3\\lib\\site-packages (from pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4->spacy) (4.12.2)\n",
      "Requirement already satisfied: charset-normalizer<4,>=2 in d:\\program files\\anaconda3\\lib\\site-packages (from requests<3.0.0,>=2.13.0->spacy) (2.0.4)\n",
      "Requirement already satisfied: idna<4,>=2.5 in d:\\program files\\anaconda3\\lib\\site-packages (from requests<3.0.0,>=2.13.0->spacy) (3.4)\n",
      "Requirement already satisfied: urllib3<3,>=1.21.1 in d:\\program files\\anaconda3\\lib\\site-packages (from requests<3.0.0,>=2.13.0->spacy) (1.26.16)\n",
      "Requirement already satisfied: certifi>=2017.4.17 in d:\\program files\\anaconda3\\lib\\site-packages (from requests<3.0.0,>=2.13.0->spacy) (2023.7.22)\n",
      "Requirement already satisfied: blis<1.3.0,>=1.2.0 in d:\\program files\\anaconda3\\lib\\site-packages (from thinc<8.4.0,>=8.3.4->spacy) (1.2.0)\n",
      "Requirement already satisfied: confection<1.0.0,>=0.0.1 in d:\\program files\\anaconda3\\lib\\site-packages (from thinc<8.4.0,>=8.3.4->spacy) (0.1.5)\n",
      "Requirement already satisfied: colorama in d:\\program files\\anaconda3\\lib\\site-packages (from tqdm<5.0.0,>=4.38.0->spacy) (0.4.6)\n",
      "Requirement already satisfied: click>=8.0.0 in d:\\program files\\anaconda3\\lib\\site-packages (from typer<1.0.0,>=0.3.0->spacy) (8.0.4)\n",
      "Requirement already satisfied: shellingham>=1.3.0 in d:\\program files\\anaconda3\\lib\\site-packages (from typer<1.0.0,>=0.3.0->spacy) (1.5.4)\n",
      "Requirement already satisfied: rich>=10.11.0 in d:\\program files\\anaconda3\\lib\\site-packages (from typer<1.0.0,>=0.3.0->spacy) (13.9.4)\n",
      "Requirement already satisfied: cloudpathlib<1.0.0,>=0.7.0 in d:\\program files\\anaconda3\\lib\\site-packages (from weasel<0.5.0,>=0.1.0->spacy) (0.21.0)\n",
      "Requirement already satisfied: smart-open<8.0.0,>=5.2.1 in d:\\program files\\anaconda3\\lib\\site-packages (from weasel<0.5.0,>=0.1.0->spacy) (5.2.1)\n",
      "Requirement already satisfied: MarkupSafe>=2.0 in d:\\program files\\anaconda3\\lib\\site-packages (from jinja2->spacy) (2.1.1)\n",
      "Requirement already satisfied: marisa-trie>=1.1.0 in d:\\program files\\anaconda3\\lib\\site-packages (from language-data>=1.2->langcodes<4.0.0,>=3.2.0->spacy) (1.2.1)\n",
      "Requirement already satisfied: markdown-it-py>=2.2.0 in d:\\program files\\anaconda3\\lib\\site-packages (from rich>=10.11.0->typer<1.0.0,>=0.3.0->spacy) (2.2.0)\n",
      "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in d:\\program files\\anaconda3\\lib\\site-packages (from rich>=10.11.0->typer<1.0.0,>=0.3.0->spacy) (2.15.1)\n",
      "Requirement already satisfied: mdurl~=0.1 in d:\\program files\\anaconda3\\lib\\site-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer<1.0.0,>=0.3.0->spacy) (0.1.0)\n",
      "Note: you may need to restart the kernel to use updated packages.\n"
     ]
    }
   ],
   "source": [
    "pip install spacy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "23fd455c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "import os\n",
    "import requests\n",
    "import re\n",
    "from tqdm import tqdm\n",
    "from collections import defaultdict\n",
    "from string import punctuation\n",
    "import spacy\n",
    "from spacy.lang.zh.stop_words import STOP_WORDS\n",
    "nlp = spacy.load('zh_core_web_sm')\n",
    "# python -m spacy download zh_core_web_sm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "6bf000bc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "train size = 8627 , test size = 2157\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|█████████████████████████████████████████████████████████████████████████████| 8627/8627 [01:03<00:00, 136.39it/s]\n",
      "100%|█████████████████████████████████████████████████████████████████████████████| 2157/2157 [00:14<00:00, 148.25it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['python', '编程', '入门', '教程']\n",
      "{'计算机类': 0, '艺术传媒类': 1, '经管类': 2}\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "class BooksDataset:\n",
    "    def __init__(self):\n",
    "        train_file, test_file = 'train.jsonl', 'test.jsonl'\n",
    "\n",
    "        # 下载数据为JSON格式，转化为Python对象\n",
    "        def read_file(file_name):\n",
    "            with open(file_name, 'r', encoding='utf-8') as fin:\n",
    "                json_list = list(fin)\n",
    "            data_split = []\n",
    "            for json_str in json_list:\n",
    "                data_split.append(json.loads(json_str))\n",
    "            return data_split\n",
    "\n",
    "        self.train_data, self.test_data = read_file(train_file),\\\n",
    "            read_file(test_file)\n",
    "        print('train size =', len(self.train_data), \n",
    "              ', test size =', len(self.test_data))\n",
    "        \n",
    "        # 建立文本标签和数字标签的映射\n",
    "        self.label2id, self.id2label = {}, {}\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                txt = data['class']\n",
    "                if txt not in self.label2id:\n",
    "                    idx = len(self.label2id)\n",
    "                    self.label2id[txt] = idx\n",
    "                    self.id2label[idx] = txt\n",
    "                label_id = self.label2id[txt]\n",
    "                data['label'] = label_id\n",
    "\n",
    "    def tokenize(self, attr='book'):\n",
    "        # 使用以下两行命令安装spacy用于中文分词\n",
    "        # pip install -U spacy\n",
    "        # python -m spacy download zh_core_web_sm\n",
    "        # 去除文本中的符号和停用词\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in tqdm(data_split):\n",
    "                # 转为小写\n",
    "                text = data[attr].lower()\n",
    "                # 符号替换为空\n",
    "                tokens = [t.text for t in nlp(text) \\\n",
    "                    if t.text not in STOP_WORDS]\n",
    "                # 这一步比较耗时，因此把tokenize的结果储存起来\n",
    "                data['tokens'] = tokens\n",
    "\n",
    "    # 根据分词结果建立词表，忽略部分低频词，\n",
    "    # 可以设置词最短长度和词表最大大小\n",
    "    def build_vocab(self, min_freq=3, min_len=2, max_size=None):\n",
    "        frequency = defaultdict(int)\n",
    "        for data in self.train_data:\n",
    "            tokens = data['tokens']\n",
    "            for token in tokens:\n",
    "                frequency[token] += 1 \n",
    "\n",
    "        print(f'unique tokens = {len(frequency)}, '+\\\n",
    "              f'total counts = {sum(frequency.values())}, '+\\\n",
    "              f'max freq = {max(frequency.values())}, '+\\\n",
    "              f'min freq = {min(frequency.values())}')    \n",
    "\n",
    "        self.token2id = {}\n",
    "        self.id2token = {}\n",
    "        total_count = 0\n",
    "        for token, freq in sorted(frequency.items(),\\\n",
    "            key=lambda x: -x[1]):\n",
    "            if max_size and len(self.token2id) >= max_size:\n",
    "                break\n",
    "            if freq > min_freq:\n",
    "                if (min_len is None) or (min_len and \\\n",
    "                    len(token) >= min_len):\n",
    "                    self.token2id[token] = len(self.token2id)\n",
    "                    self.id2token[len(self.id2token)] = token\n",
    "                    total_count += freq\n",
    "            else:\n",
    "                break\n",
    "        print(f'min_freq = {min_freq}, min_len = {min_len}, '+\\\n",
    "              f'max_size = {max_size}, '\n",
    "              f'remaining tokens = {len(self.token2id)}, '\n",
    "              f'in-vocab rate = {total_count / sum(frequency.values())}')\n",
    "\n",
    "    # 将分词后的结果转化为数字索引\n",
    "    def convert_tokens_to_ids(self):\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                data['token_ids'] = []\n",
    "                for token in data['tokens']:\n",
    "                    if token in self.token2id:\n",
    "                        data['token_ids'].append(self.token2id[token])\n",
    "\n",
    "        \n",
    "dataset = BooksDataset()\n",
    "dataset.tokenize()\n",
    "print(dataset.train_data[0]['tokens'])\n",
    "print(dataset.label2id)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "a4a21043",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['D:\\\\Program Files\\\\Anaconda3', 'D:\\\\Program Files\\\\Anaconda3\\\\Lib\\\\site-packages']\n"
     ]
    }
   ],
   "source": [
    "import site\n",
    "print(site.getsitepackages())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "9e9fc7c2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "unique tokens = 6956, total counts = 54884, max freq = 1635, min freq = 1\n",
      "min_freq = 3, min_len = 2, max_size = None, remaining tokens = 1650, in-vocab rate = 0.7944209605713869\n",
      "[18, 26, 5, 0]\n"
     ]
    }
   ],
   "source": [
    "dataset.build_vocab(min_freq=3)\n",
    "dataset.convert_tokens_to_ids()\n",
    "print(dataset.train_data[0]['token_ids'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "e4618660",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "train_X, train_Y = [], []\n",
    "test_X, test_Y = [], []\n",
    "\n",
    "for data in dataset.train_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    train_X.append(x)\n",
    "    train_Y.append(data['label'])\n",
    "for data in dataset.test_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    test_X.append(x)\n",
    "    test_Y.append(data['label'])\n",
    "train_X, train_Y = np.array(train_X), np.array(train_Y)\n",
    "test_X, test_Y = np.array(test_X), np.array(test_Y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "2013d2c0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "P(计算机类) = 0.4453460067230787\n",
      "P(艺术传媒类) = 0.26660484525327466\n",
      "P(经管类) = 0.2880491480236467\n",
      "P(教程|计算机类) = 0.5726495726495726\n",
      "P(基础|计算机类) = 0.6503006012024048\n",
      "P(设计|计算机类) = 0.606694560669456\n",
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "class NaiveBayes:\n",
    "    def __init__(self, num_classes, vocab_size):\n",
    "        self.num_classes = num_classes\n",
    "        self.vocab_size = vocab_size\n",
    "        self.prior = np.zeros(num_classes, dtype=np.float64)\n",
    "        self.likelihood = np.zeros((num_classes, vocab_size),\\\n",
    "            dtype=np.float64)\n",
    "        \n",
    "    def fit(self, X, Y):\n",
    "        # NaiveBayes的训练主要涉及先验概率和似然的估计，\n",
    "        # 这两者都可以通过计数简单获得\n",
    "        for x, y in zip(X, Y):\n",
    "            self.prior[y] += 1\n",
    "            for token_id in x:\n",
    "                self.likelihood[y, token_id] += 1\n",
    "                \n",
    "        self.prior /= self.prior.sum()\n",
    "        # laplace平滑\n",
    "        self.likelihood += 1\n",
    "        self.likelihood /= self.likelihood.sum(axis=0)\n",
    "        # 为了避免精度溢出，使用对数概率\n",
    "        self.prior = np.log(self.prior)\n",
    "        self.likelihood = np.log(self.likelihood)\n",
    "    \n",
    "    def predict(self, X):\n",
    "        # 算出各个类别的先验概率与似然的乘积，找出最大的作为分类结果\n",
    "        preds = []\n",
    "        for x in X:\n",
    "            p = np.zeros(self.num_classes, dtype=np.float64)\n",
    "            for i in range(self.num_classes):\n",
    "                p[i] += self.prior[i]\n",
    "                for token in x:\n",
    "                    p[i] += self.likelihood[i, token]\n",
    "            preds.append(np.argmax(p))\n",
    "        return preds\n",
    "\n",
    "nb = NaiveBayes(len(dataset.label2id), len(dataset.token2id))\n",
    "train_X, train_Y = [], []\n",
    "for data in dataset.train_data:\n",
    "    train_X.append(data['token_ids'])\n",
    "    train_Y.append(data['label'])\n",
    "nb.fit(train_X, train_Y)\n",
    "\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2label[i]}) = {np.exp(nb.prior[i])}')\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2token[i]}|{dataset.id2label[0]}) = '+\\\n",
    "          f'{np.exp(nb.likelihood[0, i])}')\n",
    "\n",
    "test_X, test_Y = [], []\n",
    "for data in dataset.test_data:\n",
    "    test_X.append(data['token_ids'])\n",
    "    test_Y.append(data['label'])\n",
    "    \n",
    "NB_preds = nb.predict(test_X)\n",
    "    \n",
    "for i, (p, y) in enumerate(zip(NB_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "f1c84726",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "C:\\Users\\user\\Documents\\社会网络舆情演化分析\\utils.py\n"
     ]
    }
   ],
   "source": [
    "import utils\n",
    "print(utils.__file__)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "cc6abd5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    "sys.path.append('../code')\n",
    "from my_utils import TFIDF  # utils 和系统中的 utils 库重名了，如上代码显示，因此可以将自己编写的 utils 重命名为 my_utils\n",
    "        \n",
    "tfidf = TFIDF(len(dataset.token2id))\n",
    "tfidf.fit(train_X)\n",
    "train_F = tfidf.transform(train_X)\n",
    "test_F = tfidf.transform(test_X)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "45ef1496",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "epoch-49, loss=0.1962: 100%|█| 50/50 [00:08<00:00,  5.61it/s\n"
     ]
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAGwCAYAAABB4NqyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABa/0lEQVR4nO3dd3gU5doG8Hs3nVRaEgIhBEIJhJpQQm+GLthAQQUFFUFRsSIqIB6xC9LsYEHlQxRRaug1lBAg9BZICAmBACkEUnbf74+QzW52tiVbs/fvujjXzsw7M8/OGdmHt8qEEAJERERETkRu6wCIiIiIrI0JEBERETkdJkBERETkdJgAERERkdNhAkREREROhwkQEREROR0mQEREROR0XG0dgLUplUpcuXIFvr6+kMlktg6HiIiIjCCEQF5eHkJCQiCXV73+xukSoCtXriA0NNTWYRAREVElpKWloUGDBlW+jtMlQL6+vgBKH6Cfn5+NoyEiIiJj5ObmIjQ0VPU7XlVOlwCVNXv5+fkxASIiInIw5uq+wk7QRERE5HSYABEREZHTYQJERERETocJEBERETkdJkBERETkdJgAERERkdNhAkREREROhwkQEREROR0mQEREROR0mAARERGR02ECRERERE6HCRARERE5HSZAZpRTUIzDabdsHQYREREZwATITA6l3kSXOZsxYuFu3ClS2DocIiIi0oMJkJm0rOeHO8Wlic/Os9dsHA0RERHpwwTITDzdXNC2gT8A4Ovt520cDREREenDBMiMPNxcAACHUm/ZNhAiIiLSiwmQGX30YGvV5zNX82wYCREREenDBMiMGtf1gZuLDADwz+F0G0dDREREujABMrPHu4QBABZuZT8gIiIie8UEyMzq+XuqPm8+edWGkRAREZEuTIDMbEznMNXnL+LP2DASIiIi0oUJkJl5e7iqPh+/kotreYU2jIaIiIikMAGygNnDW6k+f8M5gYiIiOwOEyALyCssUX2u6e1uw0iIiIhIChMgC3ioQwPV5083nEaxQmnDaIiIiKgiJkAWEOTniVExoart3/al2jAaIiIiqogJkIX0iwxUfZ6x+jjOX8u3YTRERESkjgmQhdT20ez70+/z7TaKhIiIiCpiAmQh7UNr2joEIiIi0oEJkIXI5TL4erpq7FMqhY2iISIiInVMgCzojYEtNLY3cWkMIiIi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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "eval_loss = 0.2868\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "\n",
    "class LR(nn.Module):\n",
    "    def __init__(self, input_dim, output_dim):\n",
    "        super(LR, self).__init__()\n",
    "        self.linear = nn.Linear(input_dim, output_dim)\n",
    "        \n",
    "    def forward(self, input_feats, labels=None):\n",
    "        outputs = self.linear(input_feats)\n",
    "        \n",
    "        if labels is not None:\n",
    "            loss_fc = nn.CrossEntropyLoss()\n",
    "            loss = loss_fc(outputs, labels)\n",
    "            return (loss, outputs)\n",
    "        \n",
    "        return outputs\n",
    "\n",
    "model = LR(len(dataset.token2id), len(dataset.label2id))\n",
    "\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torch.optim import SGD, Adam\n",
    "\n",
    "# 使用PyTorch的DataLoader来进行数据循环，因此按照PyTorch的接口\n",
    "# 实现myDataset和DataCollator两个类\n",
    "# myDataset是对特征向量和标签的简单封装便于对齐接口，\n",
    "# DataCollator用于批量将数据转化为PyTorch支持的张量类型\n",
    "class myDataset(Dataset):\n",
    "    def __init__(self, X, Y):\n",
    "        self.X = X\n",
    "        self.Y = Y\n",
    "        \n",
    "    def __len__(self):\n",
    "        return len(self.X)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        return (self.X[idx], self.Y[idx])\n",
    "\n",
    "class DataCollator:\n",
    "    @classmethod\n",
    "    def collate_batch(cls, batch):\n",
    "        feats, labels = [], []\n",
    "        for x, y in batch:\n",
    "            feats.append(x)\n",
    "            labels.append(y)\n",
    "        # 直接将一个ndarray的列表转化为张量是非常慢的，\n",
    "        # 所以需要提前将列表转化为一整个ndarray\n",
    "        feats = torch.tensor(np.array(feats), dtype=torch.float)\n",
    "        labels = torch.tensor(np.array(labels), dtype=torch.long)\n",
    "        return {'input_feats': feats, 'labels': labels}\n",
    "\n",
    "# 设置训练超参数和优化器，模型初始化\n",
    "epochs = 50\n",
    "batch_size = 128\n",
    "learning_rate = 1e-3\n",
    "weight_decay = 0\n",
    "\n",
    "train_dataset = myDataset(train_F, train_Y)\n",
    "test_dataset = myDataset(test_F, test_Y)\n",
    "\n",
    "data_collator = DataCollator()\n",
    "train_dataloader = DataLoader(train_dataset, batch_size=batch_size,\\\n",
    "    shuffle=True, collate_fn=data_collator.collate_batch)\n",
    "test_dataloader = DataLoader(test_dataset, batch_size=batch_size,\\\n",
    "    shuffle=False, collate_fn=data_collator.collate_batch)\n",
    "optimizer = Adam(model.parameters(), lr=learning_rate,\\\n",
    "    weight_decay=weight_decay)\n",
    "model.zero_grad()\n",
    "model.train()\n",
    "\n",
    "from tqdm import tqdm, trange\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# 模型训练\n",
    "with trange(epochs, desc='epoch', ncols=60) as pbar:\n",
    "    epoch_loss = []\n",
    "    for epoch in pbar:\n",
    "        model.train()\n",
    "        for step, batch in enumerate(train_dataloader):\n",
    "            loss = model(**batch)[0]\n",
    "            pbar.set_description(f'epoch-{epoch}, loss={loss.item():.4f}')\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            model.zero_grad()\n",
    "            epoch_loss.append(loss.item())\n",
    "\n",
    "    epoch_loss = np.array(epoch_loss)\n",
    "    # 打印损失曲线\n",
    "    plt.plot(range(len(epoch_loss)), epoch_loss)\n",
    "    plt.xlabel('training epoch')\n",
    "    plt.ylabel('loss')\n",
    "    plt.show()\n",
    "    \n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        loss_terms = []\n",
    "        for batch in test_dataloader:\n",
    "            loss = model(**batch)[0]\n",
    "            loss_terms.append(loss.item())\n",
    "        print(f'eval_loss = {np.mean(loss_terms):.4f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "472d2b24",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "LR_preds = []\n",
    "model.eval()\n",
    "for batch in test_dataloader:\n",
    "    with torch.no_grad():\n",
    "        _, preds = model(**batch)\n",
    "        preds = np.argmax(preds, axis=1)\n",
    "        LR_preds.extend(preds)\n",
    "            \n",
    "for i, (p, y) in enumerate(zip(LR_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "2669ace7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "NB: micro-f1 = 0.8961520630505331, macro-f1 = 0.8948572078813896\n",
      "LR: micro-f1 = 0.9142327306444136, macro-f1 = 0.9134964833213877\n"
     ]
    }
   ],
   "source": [
    "test_Y = np.array(test_Y)\n",
    "NB_preds = np.array(NB_preds)\n",
    "LR_preds = np.array(LR_preds)\n",
    "\n",
    "def micro_f1(preds, labels):\n",
    "    TP = np.sum(preds == labels)\n",
    "    FN = FP = 0\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        FN += np.sum((preds == i) & (labels != i))\n",
    "        FP += np.sum((preds != i) & (labels == i))\n",
    "    precision = TP / (TP + FP)\n",
    "    recall = TP / (TP + FN)\n",
    "    f1 = 2 * precision * recall / (precision + recall)\n",
    "    return f1\n",
    "\n",
    "def macro_f1(preds, labels):\n",
    "    f_scores = []\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        TP = np.sum((preds == i) & (labels == i))\n",
    "        FN = np.sum((preds == i) & (labels != i))\n",
    "        FP = np.sum((preds != i) & (labels == i))\n",
    "        precision = TP / (TP + FP)\n",
    "        recall = TP / (TP + FN)\n",
    "        f1 = 2 * precision * recall / (precision + recall)\n",
    "        f_scores.append(f1)\n",
    "    return np.mean(f_scores)\n",
    "\n",
    "print(f'NB: micro-f1 = {micro_f1(NB_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(NB_preds, test_Y)}')\n",
    "print(f'LR: micro-f1 = {micro_f1(LR_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(LR_preds, test_Y)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "30969c94",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
